Angular 是一個很注重模組化開發的框架
還記得我在 基礎結構說明(一) 有提到過這句話嗎?
我一直覺得這句話是在使用 Angular 開發時,很重要的心法。
因為透過 NgModule 的包裝,我們可以很容易地就能做到重複利用同樣地功能或元件,進而減少 DRY 的情形,讓我們的程式碼更好被閱讀、被維護。
而 Angular 的各個機制也都會以 NgModule 為基礎去延伸出很多好用的功能,例如昨天提到的 RoutingModule ,以及今天要分享的模組延遲載入功能。
試想我們平常在使用一個系統時,有多少頁面我們可能連進去都不會進去?或是根本就沒那個權限進去?
SPA 是一個整個系統都只有一頁的應用,它會將所有有用到的功能、頁面都打包在一起。試試看以我們自己本身所操作過最大的系統去想像,那這包 SPA 的檔案該有多大?又有多少功能與頁面是大多數使用者都不會用到的?
所以在實務上, Angular Routing 這個模組延遲載入的功能就非常重要了!!
因為這個功能可以讓使用者需要某個功能或頁面的時候,就只下載這某個功能或頁面的程式碼回來。進而使得在頁面初始化所耗費的時間更少,因只需要下載與執行當下所需之程式碼而已。當一個系統規模越大時,節省的時間與提昇的效能就越發可觀。
所以這麼好用的功能到底該怎麼使用呢?
既然這個功能的名稱叫做模組延遲載入功能,顧名思義就是只有模組才能套用這功能,
先來看一下我們目前程式的架構:

目前在我們的架構裡,只有 FeatureModule 符合這個需求,所以我們可以先從 FeatureModule 開始。
首先我們先打開 app.module.ts 這個檔案,然後從裡面把 FeatureModule 的引入給移除:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
// Routing Module
import { AppRoutingModule } from './app-routing.module';
// 移除此行
import { FeatureModule } from './feature/feature.module';
// Component
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { LoginComponent } from './login/login.component';
import { LayoutComponent } from './layout/layout.component';
@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    AboutComponent,
    LoginComponent,
    LayoutComponent
  ],
  imports: [
    BrowserModule,
    FeatureModule, // 移除此行
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
為什麼要移除呢?
因為既然是延遲載入,就不可能會是先從這邊引入阿!
接著再打開 feature-routing.module.ts ,這邊需要調整一下路由的設定:
const routes: Routes = [
  {
    path: '', // 將 feature 改成空字串
    component: FeatureComponent
  }
];
最後則是到 app-routing.module.ts 新增一個路由:
// Angular 2 ~ 7
{
  path: 'feature',
  loadChildren: './feature/feature.module#FeatureModule'
}
// Angular 8+
{
  path: 'feature',
loadChildren: () => import('./feature/feature.module').then(module => module.FeatureModule)
}
有發現到哪裡不一樣嗎?
沒錯!這次用了一個新的屬性名稱 - loadChildren ,模組延遲載入功能主要就是透過這個屬性來設定。
不過要特別留意 './feature/feature.module#FeatureModule' 這串設定的組成。在 # 之前的是,要引入的模組相對於當前這個檔案的路徑。
什麼叫相對呢?我們先來看一下圖:

從這張圖大家可以看到,當前這個檔案指的就是我們現在在設定的 app-routing.module.ts 檔,而我們要延遲載入的模組,其檔案在從當前這個 app-routing.module.ts 檔開始算起 (./) , feaure 資料夾裡 (feature/) 的 feature.module.ts 裡 (feature.module) 。
而 # 之後的則是該模組的名稱 - FeatureModule 囉。
設定好之後,我們來實際看看畫面:

當在切換頁面的時候,有看到多載入一個檔案的話,就代表我們的模組延遲載入功能有成功設定了!
不過雖然因為檔案很小所以很不明顯,但其實在換頁的時候其實有微微地頓了一下,這正是因為模組延遲載入的原因,因其是當我們要進入某個我們設定的路由時,它才會即時地去載入這個模組,所以才會停頓一下,等待載入完成之後才會進入。
不過這麼小的一個檔案都會因為需要時間載入而有所停頓的話,那如果這個模組比較龐大的話怎麼辦?
所以如果我們不能接受這個停頓時間的話,我們可以透過另外一個設定來預先載入。
預先載入跟延遲載入很像,差別只在於,延遲模組是要進入該路由的時候即時載入;預先載入是在頁面初始化的時候就把所有可延遲載入的模組透過背景非同步地下載,不會影響畫面的顯示或使用者的操作。
那要怎麼做才能把延遲載入調整為預先載入呢?
說起來其實也很簡單,只要我們找到我們最上層的路由模組 (沒意外的話,通常都是 AppRoutingModule) ,然後先從 @angular/router 引入一個名為 PreloadAllModules 的模組,像這樣:
import { PreloadAllModules } from '@angular/router';
接著再在 RouterModule.forRoot 的參數物件裡加一個名為 preloadingStrategy 的屬性,並把 PreloadAllModules 設定給它:
@NgModule({
  imports: [RouterModule.forRoot(routes, {
    useHash: true,
    preloadingStrategy: PreloadAllModules
  })],
  exports: [RouterModule],
  providers: []
})
然後畫面重整後,你就會發現在一開始就已經先把可以延遲載入的模組先載了下來:

然後換頁的時候也不會有頓了一下的感覺。
好的!所以今天我們也已經完成了延遲載入與預先載入的功能了!
接下來會再花一天的時間介紹路由基礎設定的最後一個功能,
敬請期待囉!!
大大詢問一下如果是app-routing裡面包成巢狀:
const routes: Routes = [
    {path: '', component: IndexComponent,children:[
        {path: "A", loadChildren: "./a-component#AComponentModule"},
        {path: "B", loadChildren: "./b-component#BComponentModule"},
    ]}
];
這樣也可以嗎,還是說他會一次載入A和B就失去效益了?
Hi Zaku,
可以噢!這樣設定一樣是只會在第一次進到路徑為 "A" 時載入 A ,第一次進到路徑為 "B" 時載入 B 。

感恩,太好了QQ,如果是進入IndexComponent就開始載入滿尷尬的,會缺乏一些彈性設置

path: feature_A 和 path: feature_Bfeature-routing
const routes: Routes = [
  {
    path: '',
    component: Feature_AComponent
  },
  {
    path: '',
    component: Feature_BComponent
  }
];
app-routing
{
  path: 'feature_A',
  loadChildren: './feature/feature.module#FeatureModule'
},
{
  path: 'feature_B',
  loadChildren: './feature/feature.module#FeatureModule'
}
        AppModule
            |
    -----------------
   |        |        |
Module1  Module2  Module3 (lazy loading)
                     |
                  Module4 (lazy loading)
未設定 lazy loading 前的 Module4
const routes: Routes = [
  {
    path: 'three/four'
    component: FourComponent
  }
];
改為module4-routing
const routes: Routes = [
  {
    path: ''
    component: FourComponent
  }
];
module3-routing
{
  path: '',
  children: [
    path: 'four',
    loadChildren: './four/four.module#FourModule'
  ]
}
app-routing
{
  path: 'three',
  loadChildren: './three/three.module#ThreeModule'
}
同層級下的 path 不能重複,所以你的設定會永遠只進到 Feature_AComponent
當然可以巢狀,不過你的 module3-routing 的設定會有問題。
關於巢狀路由,我之前有做一個簡單的 Sample ,雖然不是 LazyLoad ,但原理一樣,你可以再參考看看。
app-routing
{
  path: 'feature',   // 這裡要插入一個路徑
  loadChildren: './feature/feature.module#FeatureModule'
},
feature-routing
const routes: Routes = [
  {
    path: 'feature_A',
    component: Feature_AComponent
  },
  {
    path: 'feature_B',
    component: Feature_BComponent
  }
];
所以 URL 變成 feature/feature_A 和 feature/feature_B
看起來要引入一個中轉站,才能成功
2. 我看這篇的設定和我上面一樣。
還是你其實是指這篇的設定法 ?app-routing
{
  path: 'three',
  loadChildren: './three/three.module#ThreeModule'
}
將 Module3 和 Module4 合在一起
{
  path: '',
  component: ThreeComponent
},
{
  path: 'four',
  loadChildren: './four/four.module#FourModule'
  ]
}
3.我看官網的 loadChildren 是這樣
loadChildren: () => import('./four/four.module').then(mod => mod.FourModule)
而你的寫法是這樣
loadChildren: './four/four.module#FourModule'
這 2 個寫法是一樣的嗎 ? 還是一個對應 PathLocationStrategy 另一個是 HashLocationStrategy ?
這就是為什麼會有個 LayoutModule
之所以會叫 children 或是 loadChildren 是因為他有階層概念,每多一個階層就要對應多一個 <router-outlet></router-outlet> 才接的起來。
Angular 8 之後請使用以下語法:
loadChildren: () => import('./four/four.module').then(mod => mod.FourModule)